How I Used SIGUSR1 To Avoid Python Process Conflicts
Posted on Sat 28 February 2026 in Tutorial
Lately I was looking at a solution to avoid update-station check-now starting a new process that could potentially clash with the tray process. I learned how to use SIGUSR1 for IPC to avoid starting a second instance of Update Station when doing a check-now for updates.
The Problem
Update Station runs as a system tray daemon. It also accepts a check-now argument to check if updates are available and open the update window. The problem is that if the tray was already running, check-now would start a second instance of update-station unnecessarily. The tray was already there and capable of opening the update window itself.
# Old approach, always spawns a second process even if tray is running
if arg[1] == "check-now":
Data.close_session = True
StartCheckUpdate()
The Solution: IPC via SIGUSR1
Instead of spawning a second process, I wanted the check-now invocation to find the running tray and just tell it to open the window. That meant I needed three things: a way to find the running process, a way to talk to it, and a way for it to listen.
Finding the running process
The first thing I needed was a way to identify the running tray process reliably. I did that by giving it a recognizable name using setproctitle, which is a libc function on FreeBSD that changes the process name visible in ps and /proc. Python does not expose it directly but ctypes makes it easy to call:
import ctypes
import ctypes.util
libc = ctypes.CDLL(ctypes.util.find_library('c'))
libc.setproctitle(b'update-station')
After this call the process cmdline becomes ['python: update-station'], something unique I could search for. Then I used psutil to scan running processes and find it:
import os
import psutil
def find_running_instance_by_name() -> int:
my_pid = os.getpid()
for proc in psutil.process_iter(['pid', 'cmdline']):
try:
cmdline = proc.info['cmdline']
if (cmdline and len(cmdline) == 1
and 'update-station' in cmdline[0]
and proc.info['pid'] != my_pid
and proc.is_running()):
return proc.info['pid']
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
return 0
Talking to it
Once I had the PID, I needed a way to tell the running tray to open the update window. I used SIGUSR1 for that. It is a user-defined signal that does nothing by default, which makes it perfect for this kind of custom IPC. Sending it is just one line:
import signal
def send_signal_to_instance(pid: int) -> bool:
try:
os.kill(pid, signal.SIGUSR1)
return True
except OSError:
return False
Making the tray listen
The last piece was making the running tray respond to the signal. The tricky part here is that signals arrive outside the GTK main loop, so you cannot touch any widgets directly from a signal handler or you will get crashes. The solution is GLib.idle_add(), which schedules the work to run safely on the main thread:
from gi.repository import GLib
def signal_check_now(signum, frame):
def start_check():
StartCheckUpdate()
Data.system_tray.tray_icon().set_visible(False)
return False
Data.stop_pkg_refreshing = True
GLib.idle_add(start_check)
signal.signal(signal.SIGUSR1, signal_check_now)
Putting it all together
With all three pieces in place, the check-now logic became simple. If a running instance is found, signal it and exit. If not, open the window ourselves:
existing_pid = find_running_instance_by_name()
if arg[1] == "check-now":
if existing_pid > 0:
if send_signal_to_instance(existing_pid):
sys.exit(0)
# No existing instance, open the window ourselves
Data.close_session = True
StartCheckUpdate()
The key insight is that the check-now invocation never needs to create a window itself. It just tells the process that is already running to open it.
The result is a simpler check-now flow. Before, it had to go through a lot of verification logic to avoid clashing with the running tray. Now it does not need any of that, because it just signals the process that is already running to do the work.