Python3 Gtk3 Thread Example
Quick Python3 Gtk3 app to execute Python scripts to test long running threads in Gtk3 app. Some things could be better, like the python paths collection and displaying the PID, but I was focused on running threads without freezing the GUI and it just kind of happened. Hope it’s useful to someone.
#!/usr/bin/python3 import sys if sys.version_info[0] < 3: raise Exception("Python 3 or a more recent version is required.") if 'linux' not in sys.platform: raise Exception("Linux is required.") try: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gio, GLib, GObject except: raise Exception("Missing 'gi' [Gtk 3].") try: import psutil except: raise Exception("Missing 'psutil', try sudo apt install python3-psutil.") import os import os.path import fnmatch import pickle import threading import time import getpass from subprocess import PIPE ''' Find all Python executables on Linux machine. Allow easy execution of Python scripts from GUI, because I'm lazy... ''' GObject.threads_init() class Worker(threading.Thread): def __init__(self, cmd, callback): threading.Thread.__init__(self) self.cmd = cmd self.callback = callback def run(self): if not self.cmd: _get_execs() # Get Python execs. GObject.idle_add(self.callback) else: msg,err = _process(self.cmd) GObject.idle_add(self.callback,[msg,err]) def app_load(): p = '/home/%s/.pyexec' % getpass.getuser() if not os.path.exists(p): os.mkdir(p) p = os.path.join(p,'execs.bin') with open(p, 'rb') as stream: data = stream.read() execs = pickle.loads(data) return execs def update_execs(data): p = '/home/%s/.pyexec' % getpass.getuser() if not os.path.exists(p): os.mkdir(p) p = os.path.join(p,'execs.bin') with open(p, 'wb') as output: output.write(data) def load_user(): user = None p = '/home/%s/.pyexec/user.bin' % getpass.getuser() if os.path.exists(p): with open(p, 'rb') as stream: data = stream.read() user = pickle.loads(data) return user def save_user(data): p = '/home/%s/.pyexec' % getpass.getuser() if not os.path.exists(p): os.mkdir(p) p = os.path.join(p,'user.bin') data = pickle.dumps(data) with open(p, 'wb') as output: output.write(data) def search(where='/', find='python*[!-config]'): for dirpath, dirnames, filenames in os.walk(where): if 'flatpak' in dirpath.lower(): continue if 'timeshift' in dirpath.lower(): continue if 'bin' not in dirpath.lower(): continue for filename in filenames: file_path = os.path.join(dirpath, filename) if not os.access(file_path, os.X_OK): continue if not fnmatch.fnmatch(filename, find): continue if not os.path.islink(file_path): continue yield file_path def _get_execs(): python_execs = [item for item in search()] dumped = pickle.dumps(python_execs) update_execs(dumped) def _process(cmd): proc = psutil.Popen(cmd, stdout=PIPE, stderr=PIPE) print('NAME: %s PID = %s' % (proc.name(), str(proc.pid).split()[0])) msg, err = proc.communicate() return msg, err class Executor(Gtk.Window): ''' Main app window class. ''' def __init__(self): Gtk.Window.__init__(self, title="Python Executor") self.set_border_width(10) self.set_default_size(400, 350) self.python_list = None self.proj_path = None self.script = None self.selected = None self.cmd = None self.msg = '' self.err = '' self.pid = '' self.versions = ['/usr/bin/python'] # placeholder # If the python list does not exist create, else populate versions. self.app_load_check() # Load user settings. self.proj_path_default = "/home/%s/Documents/Python" % getpass.getuser() self.script_default = "script.py" self.cbox_index = 0 user_settings = load_user() if user_settings: self.cbox_index, self.proj_path_default, self.script_default = user_settings # Define header add buttons. header = Gtk.HeaderBar(title="Python Executor") header.props.show_close_button = True self.set_titlebar(header) # Button to refresh python list. refresh_button = Gtk.Button() icon_refresh = Gio.ThemedIcon(name="reload") image_refresh = Gtk.Image.new_from_gicon(icon_refresh, Gtk.IconSize.BUTTON) refresh_button.add(image_refresh) refresh_button.set_tooltip_text("Refresh environments") refresh_button.connect("clicked", self.on_refresh_clicked) header.pack_start(refresh_button) # Kill button. kill_button = Gtk.Button() icon_kill = Gio.ThemedIcon(name="process-stop") image_kill = Gtk.Image.new_from_gicon(icon_kill, Gtk.IconSize.BUTTON) kill_button.add(image_kill) kill_button.set_tooltip_text("Kill Process") kill_button.connect("clicked", self.on_kill_clicked) header.pack_start(kill_button) # Execute button. run_button = Gtk.Button() icon_run = Gio.ThemedIcon(name="run-build") image_run = Gtk.Image.new_from_gicon(icon_run, Gtk.IconSize.BUTTON) run_button.add(image_run) run_button.set_tooltip_text("Run") run_button.connect("clicked", self.on_run_clicked) header.pack_start(run_button) # Define layout. self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) self.pybox = Gtk.Box(spacing=10) self.prjbox = Gtk.Box(spacing=10) self.scrbox = Gtk.Box(spacing=10) self.add(self.box) # Python execs. py_label = Gtk.Label() py_label.set_text("Python") py_label.set_justify(Gtk.Justification.LEFT) py_label.set_hexpand(False) self.py_store = Gtk.ListStore(str) for ver in self.versions: if isinstance(ver, str): self.py_store.append([ver]) self.python_list = Gtk.ComboBox.new_with_model(self.py_store) self.python_list.connect("changed", self.on_changed) self.python_list.set_hexpand(False) renderer_text = Gtk.CellRendererText() self.python_list.pack_start(renderer_text, True) self.python_list.add_attribute(renderer_text, "text", 0) self.python_list.set_active(self.cbox_index) self.pybox.pack_start(py_label, False, False, 0) self.pybox.pack_end(self.python_list, False, True, 0) self.box.pack_start(self.pybox, False, True, 0) # Project Path. prj_label = Gtk.Label() prj_label.set_text("Project ") prj_label.set_justify(Gtk.Justification.LEFT) prj_label.set_hexpand(False) self.proj_path = Gtk.Entry() self.proj_path.set_text(self.proj_path_default ) self.proj_path.set_hexpand(True) self.prjbox.pack_start(prj_label, False, True, 0) self.prjbox.pack_end(self.proj_path, False, True, 0) self.box.pack_start(self.prjbox, False, True, 0) # Script scr_label = Gtk.Label() scr_label.set_text("Script ") scr_label.set_justify(Gtk.Justification.LEFT) scr_label.set_hexpand(False) self.script = Gtk.Entry() self.script.set_text(self.script_default) self.script.set_hexpand(True) self.scrbox.pack_start(scr_label, False, True, 0) self.scrbox.pack_end(self.script, False, True, 0) self.box.pack_start(self.scrbox, False, True, 0) # Message area. scrolledwindow = Gtk.ScrolledWindow() scrolledwindow.set_hexpand(True) scrolledwindow.set_vexpand(True) self.message_view = Gtk.TextView() self.textbuffer = self.message_view.get_buffer() self.tag_err = self.textbuffer.create_tag('err', foreground="#FF0000") self.tag_msg = self.textbuffer.create_tag('msg', foreground="#FFFFFF") scrolledwindow.add(self.message_view) self.box.pack_start(scrolledwindow, True, True, 0) self.show_all() def app_load_check(self): '''Fetch Python paths.''' p = '/home/%s/.pyexec' % getpass.getuser() if not os.path.exists(p): os.mkdir(p) p = os.path.join(p,'execs.bin') if not os.path.exists(p): thread = Worker(None, self.end_refresh) thread.start() else: self.versions = app_load() def on_refresh_clicked(self, widget): '''Refresh Python paths.''' thread = Worker(None, self.end_refresh) thread.start() def end_refresh(self): '''Thread callback.''' self.versions = app_load() for ver in self.versions: if ver not in self.py_store: if isinstance(ver, str): self.py_store.append([ver]) def on_changed(self, widget): '''Selected Python path.''' index = widget.get_active() self.selected = widget.get_model()[index][0] def on_kill_clicked(self, widget): kill_msg = '\n* kill: Nothing to do.' end_iter = self.textbuffer.get_end_iter() for process in psutil.process_iter(): if process.cmdline() == self.cmd: process.terminate() kill_msg = '\n* %s: %s killed' % (process.pid, process.cmdline()) self.textbuffer.insert(end_iter, kill_msg) break def on_run_clicked(self, widget): '''Execute Python script.''' ok = True self.cmd = None python_path = self.selected python_path_id = self.python_list.get_active() if not python_path: ok = False self.textbuffer.set_text('Python exec not defined.') if not os.path.exists(python_path): ok = False self.textbuffer.set_text('Python exec not found: %s' % python_path) script_path = self.proj_path.get_text() script = self.script.get_text() full_script_path = os.path.join(script_path,script) if not os.path.exists(full_script_path): ok = False self.textbuffer.set_text('Script not found: %s' % full_script_path) if ok: user_set = [python_path_id, script_path, script] save_user(user_set) self.textbuffer.set_text(self.pid) self.pid = 'PID: Unkown. Possibly ended quickly.' self.cmd = [python_path, full_script_path] thread = Worker(self.cmd, self.messages) thread.start() for process in psutil.process_iter(): if process.cmdline() == self.cmd: self.pid = 'PID = %s: %s' % (process.pid, process.cmdline()) self.textbuffer.set_text(self.pid) break def messages(self,rvals): r_err = rvals[-1].decode() r_ok = rvals[0].decode() self.err = '\nResults:\n\n%s' % r_err self.msg = '\nResults:\n\n%s' % r_ok start_iter = self.textbuffer.get_start_iter() end_iter = self.textbuffer.get_end_iter() self.textbuffer.remove_all_tags(start_iter,end_iter) if r_err: self.textbuffer.insert_with_tags(end_iter, self.err, self.tag_err) elif r_ok: if 'exception' in r_ok.lower() or 'error' in r_ok.lower() and 'traceback' in r_ok.lower(): self.textbuffer.insert_with_tags(end_iter, self.msg, self.tag_err) else: self.textbuffer.insert_with_tags(end_iter, self.msg, self.tag_msg) self.message_view.show() if __name__ == '__main__': win = Executor() win.connect("destroy", Gtk.main_quit) win.show_all() Gtk.main()