diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef77bdf..539836e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-20.04, windows-latest, macOS-13] + os: [ubuntu-20.04, windows-latest, macos-14] python_version: [3.9] steps: - uses: actions/checkout@v3 diff --git a/aw_qt/main.py b/aw_qt/main.py index 314227d..c5b72f7 100644 --- a/aw_qt/main.py +++ b/aw_qt/main.py @@ -9,6 +9,7 @@ from time import sleep import click +from PyQt6.QtCore import QLockFile from aw_core.log import setup_logging from .manager import Manager @@ -17,6 +18,35 @@ logger = logging.getLogger(__name__) +def _acquire_single_instance_lock(testing: bool) -> QLockFile: + """Ensure only one instance of aw-qt runs at a time. + + Uses QLockFile for cross-platform single-instance enforcement. + The returned lock must be kept alive for the duration of the process. + Exits with code 1 if another instance is already running. + """ + import aw_core.dirs + + data_dir = aw_core.dirs.get_data_dir("aw-qt") + suffix = "-testing" if testing else "" + lock_path = os.path.join(data_dir, f"aw-qt{suffix}.lock") + + lock = QLockFile(lock_path) + lock.setStaleLockTime(0) # Only release when the process explicitly unlocks + + if not lock.tryLock(100): + if lock.error() == QLockFile.LockError.LockFailedError: + _ok, pid, _hostname, _appname = lock.getLockInfo() + msg = f"Another instance of aw-qt is already running (PID {pid}). Exiting." + else: + msg = f"Failed to acquire instance lock ({lock.error()}). Exiting." + logger.warning(msg) + print(msg) + sys.exit(1) + + return lock + + @click.command("aw-qt", help="A trayicon and service manager for ActivityWatch") @click.option( "--testing", is_flag=True, help="Run the trayicon and services in testing mode" @@ -56,6 +86,9 @@ def main( if platform.system() == "Darwin": subprocess.call("syslog -s 'aw-qt successfully started logging'", shell=True) + # Prevent multiple instances from running simultaneously + _lock = _acquire_single_instance_lock(testing) # noqa: F841 (must stay alive) + # Create a process group, become its leader # TODO: This shouldn't go here if sys.platform != "win32":