Files
voculinux/src/vocalinux/single_instance.py
T
soubarnak ea7be5f111 feat(identity): rename user-visible identity to voculinux with one-time legacy import
The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
2026-11-06 01:29:39 +05:30

101 lines
2.9 KiB
Python

"""
Single instance enforcement for Voculinux.
Uses file locking to ensure only one instance runs at a time.
Works across sessions and handles stale locks from crashed processes.
"""
import fcntl
import logging
import os
import sys
from pathlib import Path
from typing import Optional
from .utils.paths import data_dir
logger = logging.getLogger(__name__)
# Lock file location: $XDG_DATA_HOME/voculinux/ (defaults to ~/.local/share/voculinux/)
LOCK_FILE_DIR = Path(data_dir())
LOCK_FILE_PATH = LOCK_FILE_DIR / "instance.lock"
# Global lock file handle
_lock_file: Optional[int] = None
def _get_lock_file_fd() -> int:
"""Get or create the lock file descriptor."""
global _lock_file
if _lock_file is not None:
return _lock_file
# Ensure directory exists
LOCK_FILE_DIR.mkdir(parents=True, exist_ok=True)
# Open lock file (create if doesn't exist)
# O_RDWR: read/write, O_CREAT: create if not exists
fd = os.open(str(LOCK_FILE_PATH), os.O_RDWR | os.O_CREAT, 0o644)
_lock_file = fd
return fd
def acquire_lock() -> bool:
"""
Acquire the single instance lock.
Returns:
True if lock acquired (first instance), False if another instance is running.
If another instance is running, logs a message and returns False.
"""
try:
fd = _get_lock_file_fd()
# Try to acquire exclusive lock (non-blocking)
# LOCK_EX: exclusive lock
# LOCK_NB: non-blocking (fail immediately if lock held by another process)
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except (OSError, IOError):
# Lock is held by another process
logger.warning("Another instance of Voculinux is already running. Exiting.")
print("Another instance of Voculinux is already running.", file=sys.stderr)
print(
f"If you're sure no other instance is running, delete: {LOCK_FILE_PATH}",
file=sys.stderr,
)
return False
# Lock acquired - write our PID for debugging purposes
os.ftruncate(fd, 0)
os.write(fd, f"{os.getpid()}\n".encode())
os.lseek(fd, 0, os.SEEK_SET)
logger.info("Single instance lock acquired")
return True
except Exception as e:
# Unexpected error - log and allow startup (fail open but warn)
logger.error(f"Error acquiring single instance lock: {e}")
return True
def release_lock():
"""Release the single instance lock."""
global _lock_file
if _lock_file is not None:
try:
# Release lock
fcntl.flock(_lock_file, fcntl.LOCK_UN)
os.close(_lock_file)
_lock_file = None
logger.debug("Single instance lock released")
except (OSError, IOError) as e:
logger.warning(f"Failed to release lock: {e}")
# Ensure clean state
_lock_file = None