Skip to content

Commit 7967ff8

Browse files
fix: enforce single-instance using QLockFile (#117)
* fix: enforce single-instance using QLockFile Uses Qt's cross-platform QLockFile to prevent multiple instances of aw-qt from running simultaneously. When a second launch is attempted, it logs a warning (including the PID of the existing instance) and exits with code 1. Testing mode gets its own lock file so it doesn't conflict with a normal production instance. Fixes ActivityWatch/activitywatch#1176 * fix(ci): unpack 4 values from getLockInfo() and fix macOS runner name * ci: switch to macos-14 runner (macos-13 config deprecated)
1 parent 1cde7bf commit 7967ff8

File tree

1 file changed

+33
-0
lines changed

1 file changed

+33
-0
lines changed

aw_qt/main.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from time import sleep
1010

1111
import click
12+
from PyQt6.QtCore import QLockFile
1213
from aw_core.log import setup_logging
1314

1415
from .manager import Manager
@@ -17,6 +18,35 @@
1718
logger = logging.getLogger(__name__)
1819

1920

21+
def _acquire_single_instance_lock(testing: bool) -> QLockFile:
22+
"""Ensure only one instance of aw-qt runs at a time.
23+
24+
Uses QLockFile for cross-platform single-instance enforcement.
25+
The returned lock must be kept alive for the duration of the process.
26+
Exits with code 1 if another instance is already running.
27+
"""
28+
import aw_core.dirs
29+
30+
data_dir = aw_core.dirs.get_data_dir("aw-qt")
31+
suffix = "-testing" if testing else ""
32+
lock_path = os.path.join(data_dir, f"aw-qt{suffix}.lock")
33+
34+
lock = QLockFile(lock_path)
35+
lock.setStaleLockTime(0) # Only release when the process explicitly unlocks
36+
37+
if not lock.tryLock(100):
38+
if lock.error() == QLockFile.LockError.LockFailedError:
39+
_ok, pid, _hostname, _appname = lock.getLockInfo()
40+
msg = f"Another instance of aw-qt is already running (PID {pid}). Exiting."
41+
else:
42+
msg = f"Failed to acquire instance lock ({lock.error()}). Exiting."
43+
logger.warning(msg)
44+
print(msg)
45+
sys.exit(1)
46+
47+
return lock
48+
49+
2050
@click.command("aw-qt", help="A trayicon and service manager for ActivityWatch")
2151
@click.option(
2252
"--testing", is_flag=True, help="Run the trayicon and services in testing mode"
@@ -56,6 +86,9 @@ def main(
5686
if platform.system() == "Darwin":
5787
subprocess.call("syslog -s 'aw-qt successfully started logging'", shell=True)
5888

89+
# Prevent multiple instances from running simultaneously
90+
_lock = _acquire_single_instance_lock(testing) # noqa: F841 (must stay alive)
91+
5992
# Create a process group, become its leader
6093
# TODO: This shouldn't go here
6194
if sys.platform != "win32":

0 commit comments

Comments
 (0)