It's not necessary to use gobject.timeout_add()
; you can just use the threading
module, which specializes in threads, and is simpler than trying to use GTK.
Using `threading` to call a function
The nice thing about using threading
is that, even though it's not part of the Gtk group of modules, it still doesn't block Gtk's main loop. Here is a simple example of a simple timeout thread using threading
:
import threading
def function():
print("Hello!")
# Create a timer
timer = threading.Timer(
1, # Interval (in seconds; can be a float) after which to call the function
function # Function to call after time interval
)
timer.start() # Start the timer
This calls function()
once after a period of 1 second, and then exits.
However, in your case, you want to call the function multiple times, to repeatedly check the state of the logs. For this, you can recreate the timer inside function()
, and run it again:
import threading
def function():
global timer
print("Hello!")
# Recreate and start the timer
timer = threading.Timer(1, function)
timer.start()
timer = threading.Timer(1, function)
timer.start()
Now to check if any new lines have been added to the log, the program needs to read the log and then compare the lines of the most recent reading to the lines of the previous reading.
Reading the lines, and using `difflib` to compare them
First of all, to compare the lines at all, they need to be stored in two lists: one containing the most recently read set of lines, the other containing the previous set of lines. Here is an example of code that reads the output of the log, stores the lines in a list, and then prints the list:
from subprocess import PIPE, Popen
# The list storing the lines of the log
current_lines = []
# Read the log
p = Popen(["journalctl", "--user-unit=appToMonitor"], stdout=PIPE)
with p.stdout:
for line in iter(p.stdout.readline, b''):
current_lines.append(line.decode("utf-8")) # Add the lines of the log to the list
p.wait()
print(current_lines)
Note that line.decode("uft-8")
must be used, because the output of p.stdout.readline
is in bytes. Use whatever encoding your program uses; utf-8
is just a common encoding.
You can then use the difflib
module to compare the two lists. Here is an example program that compares two lists of lines, and prints whatever lines are in the second list and not in the first:
import difflib
# Two lists to compare
last_lines = ["Here is the first line\n"]
current_lines = ["Here is the first line\n", "and here is the second line"]
# Iterate through all the lines, and check for new ones
for line in difflib.ndiff(last_lines, current_lines):
# Print the line only if it was not in last_lines
if line[0] == "+": # difflib inserts a "+" for every addition
print("Line added:", line.replace("+ ", "")) # Remove the "+" from the line and print it
Great, but can how do we put all this into one program?
The finale: putting all these concepts into one program that reads the output from the log every second, and adds any new lines to the text widget in the window. Here is a simple Gtk application that does just that:
import difflib
import gi
import threading
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
from subprocess import PIPE, Popen
class App(Gtk.Window):
"""The application window."""
def __init__(self):
Gtk.Window.__init__(self)
self.connect("delete-event", self.quit)
# The list of lines read from the log file
self.current_lines = []
# The list of previously read lines to compare to the current one
self.last_lines = []
# The box to hold the widgets in the window
self.box = Gtk.VBox()
self.add(self.box)
# The text widget to output the log file to
self.text = Gtk.TextView()
self.text.set_editable(False)
self.box.pack_start(self.text, True, True, 0)
# A button to demonstrate non-blocking
self.button = Gtk.Button.new_with_label("Click")
self.box.pack_end(self.button, True, True, 0)
# Add a timer thread
self.timer = threading.Timer(0.1, self.read_log)
self.timer.start()
self.show_all()
def quit(self, *args):
"""Quit."""
# Stop the timer, in case it is still waiting when the window is closed
self.timer.cancel()
Gtk.main_quit()
def read_log(self):
"""Read the log."""
# Read the log
self.current_lines = []
p = Popen(["journalctl", "--user-unit=appToMonitor"], stdout=PIPE)
with p.stdout:
for line in iter(p.stdout.readline, b''):
self.current_lines.append(line.decode("utf-8"))
p.wait()
# Compare the log with the previous reading
for d in difflib.ndiff(self.last_lines, self.current_lines):
# Check if this is a new line, and if so, add it to the TextView
if d[0] == "+":
self.text.set_editable(True)
self.text.get_buffer().insert_at_cursor(d.replace("+ ", ""))
self.text.set_editable(False)
self.last_lines = self.current_lines
# Reset the timer
self.timer = threading.Timer(1, self.read_log)
self.timer.start()
if __name__ == "__main__":
app = App()
Gtk.main()
The button demonstrates that threading
doesn't block execution while waiting; you can click it all you want, even while the program is reading the log!